# Copyright (C) 2008 Thibauld Favre <tfavre@gmail.com>
#
# Author:
#  Thibauld Favre
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA 02111-1307 USA

import downloader
import urllib
import logging
import threading
import unzip
import shutil
import os
import sys
import smcagent
from xmlnode import XMLNode
from M2Crypto import SMIME, BIO

logging.basicConfig()
LOG = logging.getLogger(__name__)
LOG.setLevel(logging.DEBUG)

class RqCodeVerificationError(Exception): pass
class RqDownloadFileError(Exception): pass

class Requests:
    def __init__(self):
        self.waiting = []
        self.processing = None
        self.finished = []

    def add(self,req):
        self.waiting.append({'id':req.id,'req':req})

    def schedule(self):
        if not self.processing:
            self.processing = self.waiting.pop(0)
            self.processing['req'].start()

    def finish(self):
        while len(self.waiting)!=0:
            req=self.waiting.pop(0)
            self.terminated(req['id'],'aborted')
            req=self.waiting.pop(0)
        if self.processing: self.processing['req'].join()

    def terminated(self,id):
        self.finished.append(self.processing.copy())
        self.processing = None

class Request(threading.Thread):
    def __init__(self,api,rsp,code_url,code_signed,code_cert,device_token,callback):
        threading.Thread.__init__(self)
        self.__api = api
        self.callback = callback
        self.id = rsp.request[0]['rq_id']
        self.name = rsp.request[0]['name']
        self.local_config = {
                'device_token':device_token,
                'api_key': self.__api.apiKey
                }
        self.code = Code(code_url,code_signed,code_cert)
        self.params = {}
        self.files_to_download = {}
        self.endStatus = None
        self.files = {}
        self.downloader = downloader.Downloader()
        for item in rsp.request[0].files[0].file:
            self.files_to_download[item.name[0].elementText] = item.url[0].elementText

        for item in rsp.request[0].configuration[0].property:
            self.params[item.name[0].elementText] = item.value[0].elementText

    def download_files(self):
        for file in self.files_to_download.keys():
            LOG.debug("downloading file: "+file)
            remote_url = self.files_to_download[file]
            local_url = self.downloader.dl_file(remote_url[remote_url.rfind('/')+1:],remote_url)
            if local_url == '': raise RqDownloadFileError, file
            self.files[file] = {'path': local_url}

    def run(self):
        try:
            self.download_files()
            self.code.verify()
            self.code.prepare()
            self.local_config['data_dir'] = self.code.data_dir
            api = ExecutionAPI(self.__api,self.local_config)
            ctx = ExecutionContext(self.code.source,self.files,self.params,self.local_config,api)
            ctx.execute()
            self.endStatus = 'ok'
        except RqCodeVerificationError, err:
            self.endStatus = 'error'
            LOG.debug("RequestCodeVerificationError")
        except RqDownloadFileError, err:
            self.endStatus = 'error'
            LOG.debug("RequestDownloadFileError")
        finally:
            self.callback(self.id,self.endStatus)

class Code:
    def __init__(self,url,signed,cert):
        self.url = url
        self.signed = signed
        self.cert = cert
        self.manifest = None
        self.code_dir = None
        self.data_dir = None

    def verify(self):
        if self.signed and self.url:
            LOG.debug("signed code %s " % self.signed)
            LOG.debug("code url %s " % self.url)
            p7, data = SMIME.smime_load_pkcs7(str(self.url+".sig"))
            try:
                code_zip = open(self.url,'wb')
                code_zip.write(self.cert.verify(p7))
                code_zip.close()
            except: 
                self.url = None
                raise RqCodeVerificationError, "Code check failed"
            return True
        elif not self.url:
            raise RqCodeVerificationError, "No code available"

    def unpack(self):
        zipHandler = unzip.unzip()
        self.code_dir = os.path.expanduser(os.path.join(smcagent.code_cache,"tmp"))
        try: shutil.rmtree(self.code_dir)
        except OSError: pass
        os.mkdir(self.code_dir)
        zipHandler.extract(self.url,self.code_dir)

    def prepare(self):
        self.unpack()
        mnf_file = open(os.path.join(self.code_dir,"manifest.xml"),'r')
        data = mnf_file.read()
        mnf_file.close()
        mnf = XMLNode.parseXML(data)
        target_dir = os.path.expanduser(os.path.join(smcagent.code_cache,mnf.name[0].elementText))
        try: shutil.rmtree(target_dir)
        except OSError: pass
        self.data_dir = os.path.expanduser(os.path.join(smcagent.data_cache,mnf.name[0].elementText))
        if not os.path.isdir(self.data_dir): os.mkdir(self.data_dir)
        shutil.move(self.code_dir,target_dir)
        self.code_dir = target_dir
        sys.path.insert(0,self.code_dir)
        self.main = mnf.main[0].elementText
        src_file = open(os.path.join(self.code_dir,self.main),'r')
        self.source = src_file.read()
        src_file.close()

class ExecutionAPI:
    def __init__(self,api,local_config):
        self.__api = api
        self.__local_config = local_config

    def encode_and_sign(self,params,join=True):
        return self.__api.encode_and_sign(params,join)

class ExecutionContext:
    def __init__(self, code, files, params, local_config,api):
        self.files = files
        self.code = code
        self.params = params
        self.local_config = local_config
        self.api = api

    def execute(self):
        exec self.code in self.__dict__
